/*
 * Copyright (C) 2008-2009 the original author or authors
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *         http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.google.code.liquidform;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.List;

import com.google.code.liquidform.conditions.ConditionalExpression;
import com.google.code.liquidform.internal.AliasFactory;
import com.google.code.liquidform.internal.ObjectUtils;
import com.google.code.liquidform.internal.managedobjects.AliasedFrameworkManagedObject;
import com.google.code.liquidform.internal.managedobjects.CollectionAssociationPath;
import com.google.code.liquidform.internal.managedobjects.FrameworkManagedObject;

/**
 * Represents the <tt>FROM</tt> part of the query. Multiple instances of
 * {@link FromClause} can be chained to form a cartesian product. Technically,
 * referring to the JSR220 specification, an instance of {@link FromClause}
 * represents an "identification_variable_declaration", <i>ie.</i> an alias that
 * may be joined (using either inner or outer joins) to multiple other property
 * paths.
 * 
 * <p>
 * Users do not directly create instances of this class, rather the first clause
 * is created by using {@link SelectClause#from(Class)}. Other variable
 * declarations are then appended using this class' {@link #andFrom(Class)}
 * construct.
 * </p>
 * 
 * @param <T>
 *            the type that this fragment is aliasing
 * @param <S>
 *            the type that was selected by the {@link SelectClause SELECT
 *            construct}
 * 
 */
public class FromClause<T, S> extends AbstractClause implements SubQuery<S> {

	/**
	 * Represents a unique join against which this {@link FromClause} is made. A
	 * join is either an <tt>(inner) join</tt> or a <tt>(left) outer (join)</tt>
	 * . A join is also either a standard join, or a fetch join. Instances of
	 * this class are added to the owning {@link FromClause} array of joins.
	 * 
	 * @param <J>
	 */
	public class JoinClause<J> {
		private final boolean fetch;

		protected J joinIdentificationVariable;

		private JoinType joinType;

		private JoinClause(J joinIdentificationVariable, JoinType joinType,
				boolean fetch) {
			this.joinIdentificationVariable = joinIdentificationVariable;
			this.joinType = joinType;
			this.fetch = fetch;
		}

		public <K> NotFetchingJoinClause<K> innerJoin(
				K joinIdentificationVariable) {
			NotFetchingJoinClause<K> joinClause = new NotFetchingJoinClause<K>(
					joinIdentificationVariable, FromClause.JoinType.INNER);
			joins.add(joinClause);
			return joinClause;
		}

		public <K> NotFetchingJoinClause<K> innerJoin(
				Collection<K> joinIdentificationVariable) {
			NotFetchingJoinClause<K> joinClause = new NotFetchingJoinClause<K>(
					null, FromClause.JoinType.INNER);
			joins.add(joinClause);
			return joinClause;
		}

		public <K> JoinClause<K> innerJoinFetch(K joinIdentificationVariable) {
			JoinClause<K> joinClause = new JoinClause<K>(
					joinIdentificationVariable, FromClause.JoinType.INNER, true);
			joins.add(joinClause);
			return joinClause;
		}

		public <K> JoinClause<K> innerJoinFetch(
				Collection<K> joinIdentificationVariable) {
			JoinClause<K> joinClause = new JoinClause<K>(null,
					FromClause.JoinType.INNER, true);
			joins.add(joinClause);
			return joinClause;
		}

		public <K> NotFetchingJoinClause<K> leftOuterJoin(
				K joinIdentificationVariable) {
			NotFetchingJoinClause<K> joinClause = new NotFetchingJoinClause<K>(
					joinIdentificationVariable, FromClause.JoinType.LEFT);
			joins.add(joinClause);
			return joinClause;
		}

		public <K> NotFetchingJoinClause<K> leftOuterJoin(
				Collection<K> joinIdentificationVariable) {
			NotFetchingJoinClause<K> joinClause = new NotFetchingJoinClause<K>(
					null, FromClause.JoinType.LEFT);
			joins.add(joinClause);
			return joinClause;
		}

		public <K> JoinClause<K> leftOuterJoinFetch(K joinIdentificationVariable) {
			JoinClause<K> joinClause = new JoinClause<K>(
					joinIdentificationVariable, FromClause.JoinType.LEFT, true);
			joins.add(joinClause);
			return joinClause;
		}

		public <K> JoinClause<K> leftOuterJoinFetch(
				Collection<K> joinIdentificationVariable) {
			JoinClause<K> joinClause = new JoinClause<K>(null,
					FromClause.JoinType.LEFT, true);
			joins.add(joinClause);
			return joinClause;
		}

		// =========================== W H E R E ===============================
		public WhereClause<S> where(ConditionalExpression condition) {
			return new WhereClause<S>(FromClause.this, condition);
		}

		// =========================== G R O U P B Y ===========================
		public GroupByClause<S> groupBy(Object groupByItem, Object... more) {
			return new GroupByClause<S>(FromClause.this, ObjectUtils.merge(
					Object.class, groupByItem, more));
		}

		// ============================ O R D E R B Y ==========================
		public OrderByClause orderBy(Number o) {
			return this.orderBy(o, SortDirection.ASC);
		}

		public OrderByClause orderBy(Number o, SortDirection dir) {
			return new OrderByClause(FromClause.this, o, dir);
		}

		public OrderByClause orderBy(Date o) {
			return this.orderBy(o, SortDirection.ASC);
		}

		public OrderByClause orderBy(Date o, SortDirection dir) {
			return new OrderByClause(FromClause.this, o, dir);
		}

		public OrderByClause orderBy(String o) {
			return this.orderBy(o, SortDirection.ASC);
		}

		public OrderByClause orderBy(String o, SortDirection dir) {
			return new OrderByClause(FromClause.this, o, dir);
		}

		public OrderByClause orderBy(Character o) {
			return this.orderBy(o, SortDirection.ASC);
		}

		public OrderByClause orderBy(Character o, SortDirection dir) {
			return new OrderByClause(FromClause.this, o, dir);
		}

		// TODO : legal or not?
		// public OrderByClause orderBy(Enum o) {
		// return this.orderBy(o, SortDirection.ASC);
		// }
		//
		// public OrderByClause orderBy(Enum o, SortDirection dir) {
		// return new OrderByClause(FromClause.this, o, dir);
		// }

		@Override
		public String toString() {
			return FromClause.this.toString();
		}

	}

	/**
	 * Represents the two available types of joins supported by JSR220.
	 */
	private static enum JoinType {
		INNER(" inner join "), LEFT(" left outer join ");
		final String stringForm;

		private JoinType(String stringForm) {
			this.stringForm = stringForm;
		}
	}

	public class NotFetchingJoinClause<J> extends JoinClause<J> {
		private NotFetchingJoinClause(J joinIdentificationVariable,
				JoinType joinType) {
			super(joinIdentificationVariable, joinType, false);
		}

		public FromClause<T, S> as(J alias) {
			this.joinIdentificationVariable = alias;
			return FromClause.this;
		}

	}

	private T identificationVariable;

	private final List<JoinClause<?>> joins = new ArrayList<JoinClause<?>>();

	protected FromClause(AbstractClause previousClauseFragment) {
		super(previousClauseFragment);
	}

	@Override
	/* default */StringBuilder addTo(StringBuilder buffer) {
		if (previousClause instanceof FromClause) {
			buffer.append(", ");
		} else {
			buffer.append(" from ");
		}

		AliasedFrameworkManagedObject aliased = (AliasedFrameworkManagedObject) AliasFactory
				.about(identificationVariable);

		buffer.append(aliased.getTargetRepresentation()).append(" as ").append(
				aliased.getAlias());

		for (JoinClause<?> jc : joins) {
			FrameworkManagedObject jAbout = AliasFactory
					.about(jc.joinIdentificationVariable);
			String sFetch = jc.fetch ? "fetch " : "";
			buffer.append(jc.joinType.stringForm).append(sFetch).append(
					jAbout.getTargetRepresentation());

			if (!jc.fetch) {
				AliasedFrameworkManagedObject jAliased = (AliasedFrameworkManagedObject) jAbout;
				buffer.append(" as ").append(jAliased.getAlias());
			}
		}

		return buffer;
	}

	public <U> FromClause<U, S> andFrom(Class<U> type) {
		return new AbstractSchemaTypeFromClause<U, S>(this, type);
	}

	public <U> FromClause<U, S> andFrom(InCollection<U> inCollection) {
		return new InCollectionFromClause<U, S>(this, inCollection);
	}

	/**
	 * <tt>from collection</tt> is only valid for subqueries, which can't return
	 * tuples.
	 * 
	 * category invalid
	 */
	public <U> void andFrom(Collection<U> invalid) {
	}

	/**
	 * <tt>from single_valued</tt> is only valid for subqueries, which can't
	 * return tuples.
	 * 
	 * category invalid
	 */
	public <U> void andFrom(U invalid) {
	}

	public FromClause<T, S> as(T alias) {
		// TODO check that alias and (ast|inCollection) have the same
		// path
		this.identificationVariable = alias;
		return this;
	}

	public <J> NotFetchingJoinClause<J> innerJoin(J joinIdentificationVariable) {
		NotFetchingJoinClause<J> joinClause = new NotFetchingJoinClause<J>(
				joinIdentificationVariable, FromClause.JoinType.INNER);
		joins.add(joinClause);
		return joinClause;
	}

	public <J> NotFetchingJoinClause<J> innerJoin(
			Collection<J> joinIdentificationVariable) {
		NotFetchingJoinClause<J> joinClause = new NotFetchingJoinClause<J>(
				null, FromClause.JoinType.INNER);
		joins.add(joinClause);
		return joinClause;
	}

	public <J> JoinClause<J> innerJoinFetch(J joinIdentificationVariable) {
		JoinClause<J> joinClause = this.new JoinClause<J>(
				joinIdentificationVariable, FromClause.JoinType.INNER, true);
		joins.add(joinClause);
		return joinClause;
	}

	public <J> JoinClause<J> innerJoinFetch(
			Collection<J> joinIdentificationVariable) {
		J collElement = rememberUnaliasedCollection(joinIdentificationVariable);

		JoinClause<J> joinClause = this.new JoinClause<J>(collElement,
				FromClause.JoinType.INNER, true);
		joins.add(joinClause);
		return joinClause;
	}

	public <J> NotFetchingJoinClause<J> leftOuterJoin(
			J joinIdentificationVariable) {
		NotFetchingJoinClause<J> joinClause = new NotFetchingJoinClause<J>(
				joinIdentificationVariable, FromClause.JoinType.LEFT);
		joins.add(joinClause);
		return joinClause;
	};

	public <J> NotFetchingJoinClause<J> leftOuterJoin(
			Collection<J> joinIdentificationVariable) {
		NotFetchingJoinClause<J> joinClause = new NotFetchingJoinClause<J>(
				null, FromClause.JoinType.LEFT);
		joins.add(joinClause);
		return joinClause;
	};

	public <J> JoinClause<J> leftOuterJoinFetch(J joinIdentificationVariable) {
		JoinClause<J> joinClause = new JoinClause<J>(
				joinIdentificationVariable, FromClause.JoinType.LEFT, true);
		joins.add(joinClause);
		return joinClause;
	}

	public <J> JoinClause<J> leftOuterJoinFetch(
			Collection<J> joinIdentificationVariable) {
		J collElement = rememberUnaliasedCollection(joinIdentificationVariable);

		JoinClause<J> joinClause = new JoinClause<J>(collElement,
				FromClause.JoinType.LEFT, true);
		joins.add(joinClause);
		return joinClause;
	}

	/**
	 * When doing fetch joins, the collections were not aliased using
	 * {@link LiquidForm#alias(Collection, String)} and thus nothing is known
	 * about the collection element (which will serve as the identification
	 * variable of the FetchJoin. This does the job.
	 * 
	 * @return the collection element of the passed in collection, which is now
	 *         a known object to the framework (denoting a non-aliased
	 *         association path)
	 */
	private <J> J rememberUnaliasedCollection(
			Collection<J> joinIdentificationVariable) {
		CollectionAssociationPath<J> about = (CollectionAssociationPath<J>) AliasFactory
				.about(joinIdentificationVariable);
		J collElement = about.getElementProxy();
		String path = about.getPreferredStringRepresentation();
		AliasFactory.rememberAsAssociationPath(collElement, path);
		return collElement;
	}

	// =========================== W H E R E ===================================
	public WhereClause<S> where(ConditionalExpression condition) {
		return new WhereClause<S>(this, condition);
	}

	// =========================== G R O U P B Y ===============================
	public GroupByClause<S> groupBy(Object groupByItem, Object... more) {
		return new GroupByClause<S>(this, ObjectUtils.merge(Object.class,
				groupByItem, more));
	}

	// ============================ O R D E R B Y ==============================
	public OrderByClause orderBy(Object o) {
		return orderBy(o, SortDirection.ASC);
	}

	public OrderByClause orderBy(Object o, SortDirection dir) {
		return new OrderByClause(this, o, dir);
	}

}

class AbstractSchemaTypeFromClause<T, S> extends FromClause<T, S> {
	/* default */AbstractSchemaTypeFromClause(
			AbstractClause previousClauseFragment, Class<T> abstractSchemaType) {
		super(previousClauseFragment);
		this.abstractSchemaType = abstractSchemaType;
	}

	private final Class<T> abstractSchemaType;
}

class InCollectionFromClause<T, S> extends FromClause<T, S> {
	/* default */InCollectionFromClause(AbstractClause previousClauseFragment,
			InCollection<T> inCollection) {
		super(previousClauseFragment);
		this.inCollection = inCollection;
	}

	private final InCollection<T> inCollection;
}

/**
 * For subselects only.
 */
class SingleValuedFromClause<T, S> extends FromClause<T, S> {
	/* default */SingleValuedFromClause(AbstractClause previousClauseFragment,
			T singleValued) {
		super(previousClauseFragment);
		this.singleValued = singleValued;
	}

	private final T singleValued;
}

/**
 * For subselects only.
 */
class CollectionValuedFromClause<T, S> extends FromClause<T, S> {
	/* default */CollectionValuedFromClause(
			AbstractClause previousClauseFragment,
			Collection<T> collectionValued) {
		super(previousClauseFragment);
		this.collectionValued = collectionValued;
	}

	private final Collection<T> collectionValued;
}